package com.xiaobai.fileupload.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xiaobai.fileupload.dto.FileQuery;
import com.xiaobai.fileupload.entity.ChunkEntity;
import com.xiaobai.fileupload.entity.FileListEntity;
import com.xiaobai.fileupload.mapper.ChunkMapper;
import com.xiaobai.fileupload.mapper.FileListMapper;
import com.xiaobai.fileupload.result.UploadResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.io.IOException;
import java.nio.file.*;
import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@Service
public class FileServiceImpl extends ServiceImpl<ChunkMapper, ChunkEntity> implements FileService {

    private final String filePath = "D:/workpace/upload";

    @Resource
    private FileListMapper fileListMapper;

    /**
     * 上传文件块
     *
     * @param chunkEntity 文件块
     * @return true成功
     */
    public boolean uploadChunk(ChunkEntity chunkEntity) {
        Path path = Paths.get(generatePath(filePath, chunkEntity));
        try {
            Files.write(path, chunkEntity.getUpfile().getBytes());
            log.debug("文件 {} 写入成功, md5:{}", chunkEntity.getFilename(), chunkEntity.getIdentifier());
            //写入数据库
            this.save(chunkEntity);
        } catch (IOException e) {
            log.error("上传文件块失败: " + e);
            return false;
        }
        return true;
    }

    /**
     * 检查文件块
     *
     * @param chunkEntity 文件块
     * @return
     */
    public UploadResult checkChunk(ChunkEntity chunkEntity) {
        UploadResult result = new UploadResult();

        //查询本地磁盘和数据库记录选一种方式
		/*
			//直接查询本地磁盘
			String file = filePath + "/" + chunkEntity.getIdentifier() + "/" + chunkEntity.getFilename();
	    	//先判断整个文件是否已经上传过了，如果是，则告诉前端跳过上传，实现秒传
	    	if(fileExists(file)) {
	    		result.setSkipUpload(true);
	    		return result;
	    	}
    	*/

        //查询数据库记录
        //先判断整个文件是否已经上传过了，如果是，则告诉前端跳过上传，实现秒传
        QueryWrapper<FileListEntity> fileWrapper = new QueryWrapper<FileListEntity>();
        fileWrapper.lambda().eq(FileListEntity::getDelFlag, 0);
        fileWrapper.lambda().eq(FileListEntity::getIdentifier, chunkEntity.getIdentifier());

        FileListEntity fileListEntity = fileListMapper.selectOne(fileWrapper.last("limit 1"));
        if (fileListEntity != null) {
            result.setSkipUpload(true);
            result.setFileId(fileListEntity.getId());
            return result;
        }

        //如果完整文件不存在，则去数据库判断当前哪些文件块已经上传过了，把结果告诉前端，跳过这些文件块的上传，实现断点续传
        QueryWrapper<ChunkEntity> chunkWrapper = new QueryWrapper<ChunkEntity>();
        chunkWrapper.lambda().eq(ChunkEntity::getIdentifier, chunkEntity.getIdentifier());

        List<ChunkEntity> chunkList = this.list(chunkWrapper);
        //将已存在的块的chunkNumber列表返回给前端,前端会规避掉这些块
        if (!CollectionUtils.isEmpty(chunkList)) {
            List<Integer> collect = chunkList.stream().map(ChunkEntity::getChunkNumber).collect(Collectors.toList());
            result.setUploadedChunks(collect);
        }
        return result;
    }

    @Transactional(rollbackFor = Exception.class)
    public boolean merge(FileListEntity fileInfo) {
        String filename = fileInfo.getFilename();
        String file = filePath + "/" + fileInfo.getIdentifier() + "/" + filename;
        String folder = filePath + "/" + fileInfo.getIdentifier();
        boolean flag = mergeFile(file, folder, filename);
        if (!flag) {
            return false;
        }

        //当前文件已存在数据库中时,返回已存在标识
        QueryWrapper<FileListEntity> fileWrapper = new QueryWrapper<FileListEntity>();
        fileWrapper.lambda().eq(FileListEntity::getDelFlag, 0);
        fileWrapper.lambda().eq(FileListEntity::getIdentifier, fileInfo.getIdentifier());

        Integer count = fileListMapper.selectCount(fileWrapper);
        if (count <= 0) {
            fileInfo.setLocation(file);
            fileListMapper.insert(fileInfo);
        }

        //插入文件记录成功后,删除chunk表中的对应记录,释放空间
        QueryWrapper<ChunkEntity> chunkWrapper = new QueryWrapper<ChunkEntity>();
        chunkWrapper.lambda().eq(ChunkEntity::getIdentifier, fileInfo.getIdentifier());
        this.remove(chunkWrapper);

        return true;
    }

    /**
     * 查看应用列表
     */
    public IPage<FileListEntity> selectFileList(FileQuery fileQuery) {
        QueryWrapper<FileListEntity> qw = new QueryWrapper<>();
        qw.lambda().eq(FileListEntity::getDelFlag, 0);
        qw.lambda().like(StringUtils.isNotBlank(fileQuery.getName()), FileListEntity::getFilename, fileQuery.getName());
        qw.lambda().orderByDesc(FileListEntity::getCreateTime);
        return fileListMapper.selectPage(new Page<>(fileQuery.getPage(), fileQuery.getLimit()), qw);
    }


    /**
     * 功能描述:生成块文件所在地址
     */
    private String generatePath(String uploadFolder, ChunkEntity chunk) {
        StringBuilder sb = new StringBuilder();
        //文件夹地址/md5
        sb.append(uploadFolder).append("/").append(chunk.getIdentifier());
        //判断uploadFolder/identifier 路径是否存在，不存在则创建
        if (!Files.isWritable(Paths.get(sb.toString()))) {
            log.info("path not exist,create path: {}", sb.toString());
            try {
                Files.createDirectories(Paths.get(sb.toString()));
            } catch (IOException e) {
                log.error(e.getMessage(), e);
            }
        }
        //文件夹地址/md5/文件名-1
        return sb.append("/")
                .append(chunk.getFilename())
                .append("-")
                .append(chunk.getChunkNumber()).toString();
    }

    /**
     * 文件合并
     *
     * @param targetFile 要形成的文件名
     * @param folder     要形成的文件夹地址
     * @param filename   文件的名称
     */
    private boolean mergeFile(String targetFile, String folder, String filename) {
        try {
            //先判断文件是否存在
            if (fileExists(targetFile)) {
                //文件已存在
                return true;
            }

            Files.createFile(Paths.get(targetFile));
            Files.list(Paths.get(folder))
                    .filter(path -> !path.getFileName().toString().equals(filename))
                    .sorted((o1, o2) -> {
                        String p1 = o1.getFileName().toString();
                        String p2 = o2.getFileName().toString();
                        int i1 = p1.lastIndexOf("-");
                        int i2 = p2.lastIndexOf("-");
                        return Integer.valueOf(p2.substring(i2)).compareTo(Integer.valueOf(p1.substring(i1)));
                    })
                    .forEach(path -> {
                        try {
                            //以追加的形式写入文件
                            Files.write(Paths.get(targetFile), Files.readAllBytes(path), StandardOpenOption.APPEND);
                            //合并后删除该块
                            Files.delete(path);
                        } catch (IOException e) {
                            log.error(e.getMessage(), e);
                        }
                    });
        } catch (IOException e) {
            log.error("文件合并失败: ", e);
            return false;
        }
        return true;
    }

    /**
     * 根据文件的全路径名判断文件是否存在
     *
     * @param file
     * @return
     */
    private boolean fileExists(String file) {
        boolean fileExists = false;
        Path path = Paths.get(file);
        fileExists = Files.exists(path, LinkOption.NOFOLLOW_LINKS);
        return fileExists;
    }

}
